package org.cryptocoinpartners.util;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;

import org.cryptocoinpartners.schema.EntityBase;
import org.hibernate.TransientObjectException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.persist.Transactional;
import com.google.inject.persist.UnitOfWork;

/**
 * <p>
 * Because DI is not always the right idea.
 * </p>
 * <p>
 * Conventional wisdom is to inject the EntityManager into classes where you might need it. This breaks down when you need to perform database access
 * in places where injection is not available. For example, you may have a polymorphic hierarchy of entity objects that exhibit different data-access
 * behavior.
 * </p>
 * <p>
 * Aspects like persistence (aka transaction) and authorization really fit a thread-local model better. This static accessor for the EntityManager
 * gives you "always-available" access to the context.
 * </p>
 */
public class EM {
  @Inject
  // @PersistenceContext
  static Provider<EntityManager> entityManagerProvider;
  private static final int defaultBatchSize = 20;
  protected static Logger log = LoggerFactory.getLogger("org.cryptocoinpartners.staticEntityManager");
  @Inject
  protected static UnitOfWork unitOfWork;

  private static EntityManager em() {
    return entityManagerProvider.get();
  }

  public static EntityManager getEnityManager() {
    try {
      beginUnitOfWork();
      return entityManagerProvider.get();
    } finally {
      unitOfWork.end();
    }
  }

  public static void queryEach(Visitor<Object[]> handler, String queryStr, Object... params) {
    queryEach(handler, defaultBatchSize, queryStr, params);
  }

  @SuppressWarnings("ConstantConditions")
  public static void queryEach(Visitor<Object[]> handler, int batchSize, String queryStr, Object... params) {
    EntityManager em = null;
    try {
      beginUnitOfWork();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

      final Query query = em().createQuery(queryStr);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      query.setMaxResults(batchSize);
      for (int start = 0;; start += batchSize) {
        query.setFirstResult(start);
        List list = query.getResultList();
        if (list.isEmpty())
          return;
        for (Object row : list) {
          if (row.getClass().isArray() && !handler.handleItem((Object[]) row) || !row.getClass().isArray() && !handler.handleItem(new Object[]{row}))
            return;
        }
      }
    } finally {
      unitOfWork.end();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

    }
  }

  // @Transactional
  public static <T> T namedQueryZeroOne(Class<T> resultType, String namedQuery, Object... params) {
    try {
      beginUnitOfWork();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

      TypedQuery<T> query = em().createNamedQuery(namedQuery, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }

      }
      T result = query.setHint("org.hibernate.cacheable", true).getSingleResult();
      return result;
    } catch (NoResultException x) {
      return null;
    } finally {
      unitOfWork.end();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

      //  if (em() != null)
      //    em().close();
    }
  }

  public static <T> T namedQueryZeroOne(Class<T> resultType, String namedQuery, Map<String, String> properties, Object... params) {
    try {
      beginUnitOfWork();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

      TypedQuery<T> query = em().createNamedQuery(namedQuery, resultType);
      for (Map.Entry<String, String> entry : properties.entrySet()) {
        EntityGraph graph = em().getEntityGraph(entry.getValue());

        query.setHint(entry.getKey(), graph);
      }
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }

      }
      T results = query.setHint("org.hibernate.cacheable", false).getSingleResult();
      return results;
    } catch (NoResultException x) {
      return null;
    } finally {
      unitOfWork.end();
      log.trace("namedQueryZeroOne unit of work ended for thread: " + Thread.currentThread());

      //  if (em() != null)
      //    em().close();
    }
  }

  // @Transactional

  public static EntityBase find(Map<String, String> properties, EntityBase... entities) {
    boolean found = true;
    EntityBase foundEntity = null;

    try {
      beginUnitOfWork();

      for (EntityBase entity : entities)
        if (!properties.isEmpty()) {
          Map hints = new HashMap();
          for (Map.Entry<String, String> entry : properties.entrySet()) {

            EntityGraph graph = em().getEntityGraph(entry.getValue());
            hints.put(entry.getKey(), graph);

            //query.setHint(entry.getKey(), graph);
          }
          foundEntity = em().find(entity.getClass(), entity.getId(), hints);
        } else
          foundEntity = em().find(entity.getClass(), entity.getId());
      if (foundEntity != null)
        for (EntityBase entity : entities)
          log.debug(entity.getClass().getSimpleName() + ": " + entity.getId().toString() + " found in database");

      else
        for (EntityBase entity : entities)
          log.info(entity.getClass().getSimpleName() + ": " + entity.getId().toString() + " not found in database");
      return foundEntity;

    } catch (Exception | Error e) {
      found = false;
      log.error("Threw a Execpton or Error in  org.cryptocoinpartners.util.persistUtil::insert, full stack trace follows:", e);

      throw e;
      //if (PersistUtilHelper.isActive())
      //  PersistUtilHelper.rollback();
    } finally {
      unitOfWork.end();
    }
    //  if (em() != null)
    //   em().close();

  }

  public static <T> T find(Class<T> resultType, UUID id, Map<String, String> properties) {
    boolean found = true;
    T foundEntity = null;

    try {
      beginUnitOfWork();

      if (!properties.isEmpty()) {
        Map hints = new HashMap();
        for (Map.Entry<String, String> entry : properties.entrySet()) {

          EntityGraph graph = em().getEntityGraph(entry.getValue());
          hints.put(entry.getKey(), graph);

          //query.setHint(entry.getKey(), graph);
        }
        foundEntity = (T) em().find(resultType, id, hints);
      } else
        foundEntity = em().find(resultType, id);
      if (foundEntity != null)
        log.debug(id + " " + resultType.getClass().getSimpleName() + ":  found in database");

      else

        log.info(id + " " + resultType.getClass().getSimpleName() + ": not found in database");
      return foundEntity;

    } catch (Exception | Error e) {
      found = false;
      log.error("Threw a Execpton or Error in  org.cryptocoinpartners.util.persistUtil::insert, full stack trace follows:", e);

      throw e;
      //if (PersistUtilHelper.isActive())
      //  PersistUtilHelper.rollback();
    } finally {
      unitOfWork.end();
    }
    //  if (em() != null)
    //   em().close();

  }

  public static EntityBase find(EntityBase... entities) {
    boolean found = true;
    EntityBase foundEntity = null;

    try {
      beginUnitOfWork();

      for (EntityBase entity : entities)

        foundEntity = em().find(entity.getClass(), entity.getId());
      if (foundEntity != null)
        for (EntityBase entity : entities)
          log.debug(entity.getClass().getSimpleName() + ": " + entity.getId().toString() + " found in database");

      else
        for (EntityBase entity : entities)
          log.info(entity.getClass().getSimpleName() + ": " + entity.getId().toString() + " not found in database");
      return foundEntity;

    } catch (Exception | Error e) {
      found = false;
      log.error("Threw a Execpton or Error in  org.cryptocoinpartners.util.persistUtil::insert, full stack trace follows:", e);

      throw e;
      //if (PersistUtilHelper.isActive())
      //  PersistUtilHelper.rollback();
    } finally {
      unitOfWork.end();
    }
    //  if (em() != null)
    //   em().close();

  }

  // @Transactional
  public static <T> T queryZeroOne(Class<T> resultType, String queryStr, Object... params) {
    try {
      beginUnitOfWork();

      final TypedQuery<T> query = em().createQuery(queryStr, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }

      T result = query.getSingleResult();
return result;
    } catch (NoResultException x) {
      return null;
    } finally {
      unitOfWork.end();
      //         if (em != null)
      //           em.close();
    }
  }

  // @Transactional
  public static <T> T queryOne(Class<T> resultType, String queryStr, Map<String, String> properties, Object... params) {
    //   EntityManager em = em();
    //  try {

    //  try {
    return sqlQueryOne(resultType, queryStr, params);

    //     PersistUtil.queryOne(Portfolio.class, queryStr, portfolioName);

  }

  public static <T> T queryOne(Class<T> resultType, String queryStr, Object... params) {
    //   EntityManager em = em();
    //  try {

    //  try {
    return sqlQueryOne(resultType, queryStr, params);

    //     PersistUtil.queryOne(Portfolio.class, queryStr, portfolioName);

  }

  public static <T> T queryLimitOne(Class<T> resultType, String queryStr, Object... params) {
    //   EntityManager em = em();
    //  try {

    //  try {
    return sqlQueryLimitOne(resultType, queryStr, params);

    //     PersistUtil.queryOne(Portfolio.class, queryStr, portfolioName);

  }

  // @Transactional
  public static <T> T sqlQueryOne(Class<T> resultType, String queryStr, Object... params) throws NoResultException {
    //   EntityManager em = em();
    T result = null;
    try {
      beginUnitOfWork();
      log.debug("sqlQueryOne unit of work started for thread: " + Thread.currentThread());
      TypedQuery<T> query = em().createQuery(queryStr, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      result = query.getSingleResult();
      return result;
    } catch (NoResultException e) {
      //  context.getInjector().getInstance(Portfolio.class);
      // PersistUtil.insert(portfolio);
      throw e;
      //return null;

    } finally {
      unitOfWork.end();
      log.trace("sqlQueryOne unit of work ended for thread: " + Thread.currentThread());

    }
    // em.flush();
    //   if (em != null)
    //em.close();
    // }
  }

  public static <T> T sqlQueryLimitOne(Class<T> resultType, String queryStr, Object... params) throws NoResultException {
    //   EntityManager em = em();
    T result = null;
    try {
      beginUnitOfWork();
      log.debug("sqlQueryOne unit of work started for thread: " + Thread.currentThread());
      TypedQuery<T> query = em().createQuery(queryStr, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      query.setMaxResults(1);
      result = query.getSingleResult();
      return result;
    } catch (NoResultException e) {
      //  context.getInjector().getInstance(Portfolio.class);
      // PersistUtil.insert(portfolio);
      throw e;
      //return null;

    } finally {
      unitOfWork.end();
      log.trace("sqlQueryOne unit of work ended for thread: " + Thread.currentThread());

    }
    // em.flush();
    //   if (em != null)
    //em.close();
    // }
  }

  public static <T> T sqlQueryOne(Class<T> resultType, String queryStr, Map<String, String> properties, Object... params) throws NoResultException {
    //   EntityManager em = em();
    T result = null;
    try {
      beginUnitOfWork();
      log.debug("sqlQueryOne unit of work started for thread: " + Thread.currentThread());
      TypedQuery<T> query = em().createQuery(queryStr, resultType);
      for (Map.Entry<String, String> entry : properties.entrySet()) {
        EntityGraph graph = em().getEntityGraph(entry.getValue());

        query.setHint(entry.getKey(), graph);
      }
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      result = query.getSingleResult();
      return result;
    } catch (NoResultException e) {
      //  context.getInjector().getInstance(Portfolio.class);
      // PersistUtil.insert(portfolio);
      throw e;
      //return null;

    } finally {
      unitOfWork.end();
      log.trace("sqlQueryOne unit of work ended for thread: " + Thread.currentThread());

    }
    // em.flush();
    //   if (em != null)
    //em.close();
    // }
  }

  //@Transactional
  public static <T> List<T> namedQueryList(Class<T> resultType, String namedQuery, Object... params) {
    try {
      beginUnitOfWork();
      log.trace("namedQueryList unit of work ended for thread: " + Thread.currentThread());

      TypedQuery<T> query = em().createNamedQuery(namedQuery, resultType);

      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }

      }
      List<T> result = query.setHint("org.hibernate.cacheable", true).getResultList();
   return result;
    } catch (NoResultException x) {
      return null;
    } finally {
      unitOfWork.end();
      log.trace("namedQueryList unit of work ended for thread: " + Thread.currentThread());

      //  if (em() != null)
      //    em().close();
    }
  }

  public static <T> List<T> namedQueryList(Class<T> resultType, String namedQuery, Map<String, String> properties, Object... params) {
    try {
      beginUnitOfWork();
      log.trace("namedQueryList unit of work ended for thread: " + Thread.currentThread());

      TypedQuery<T> query = em().createNamedQuery(namedQuery, resultType);
      for (Map.Entry<String, String> entry : properties.entrySet()) {
        EntityGraph graph = em().getEntityGraph(entry.getValue());

        query.setHint(entry.getKey(), graph);
      }
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }

      }
      List<T> result = query.setHint("org.hibernate.cacheable", true).getResultList();
      return result;
    } catch (NoResultException x) {
      return null;
    } finally {
      unitOfWork.end();
      log.trace("namedQueryList unit of work ended for thread: " + Thread.currentThread());

      //  if (em() != null)
      //    em().close();
    }
  }

  public static <T> List<T> queryList(Class<T> resultType, String queryStr, Object... params) {
    //  EntityManager em = em();
    try {
      beginUnitOfWork();
      final TypedQuery<T> query = em().createQuery(queryStr, resultType);

      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      List<T> result = query.getResultList();
      return result;
    } catch (TransientObjectException toe) {
      log.debug("what happened");
      return null;
    } finally {
      unitOfWork.end();
      // em.clear();
      //   if (em() != null)
      //  em().close();
    }
  }

  public static <T> List<T> queryList(Class<T> resultType, String queryStr, Map<String, String> properties, Object... params) {
    //  EntityManager em = em();
    try {
      beginUnitOfWork();
      final TypedQuery<T> query = em().createQuery(queryStr, resultType);
      for (Map.Entry<String, String> entry : properties.entrySet()) {
        EntityGraph graph = em().getEntityGraph(entry.getValue());

        query.setHint(entry.getKey(), graph);
      }

      // EntityGraph graph = EM.getEnityManager().getEntityGraph("graph.Position.fills");

      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      List<T> result = query.getResultList();
      return result;
    } catch (TransientObjectException toe) {
      log.debug("what happened");
      return null;
    } finally {
      unitOfWork.end();
      // em.clear();
      //   if (em() != null)
      //  em().close();
    }
  }

  // @Transactional
  public static void insert(EntityBase entity) {
    //  EntityManager em = entityManager.get();
    //    unitOfWork.begin();
    try {
      beginUnitOfWork();

      em().persist(entity);
    } catch (Exception | Error ex) {
      throw ex;
    } finally {

      unitOfWork.end();
    }
    //  em().persist(entity);
    //em().flush();
    //unitOfWork.end();
    // TODO Auto-generated method stub

  }

  @Transactional
  private static void update(EntityBase entity) {
    // EntityManager em = entityManager.get();

    em().merge(entity);
    // TODO Auto-generated method stub

  }

  //  @Transactional
  private static void evict(EntityBase entity) {
    // EntityManager em = entityManager.get();
    beginUnitOfWork();
    em().detach(entity);
    unitOfWork.end();
    // TODO Auto-generated method stub

  }

  //@Transactional
  // @Transactional
  public static void persist(EntityBase... entities) {
    // unitOfWork.begin();
    for (EntityBase entity : entities) {
      // unitOfWork.begin();
      try {

        insert(entity);
      }
      //entityManager.get().flush();

      //entityManager.get().getTransaction().commit();
      // log.debug("persisting entity " + entity.getClass().getSimpleName() + " " + entity);
      //   entityManager.get().getTransaction().commit();
      //  } catch (OptimisticLockException ole) {
      //      log.error("Unable to merge record" + ole);
      //   }

      catch (Exception | Error ex) {

        System.out.println("Unable to perform request in " + EM.class.getSimpleName() + ":persist, full stack trace follows:" + ex);
        //       ex.printStackTrace();

        //   } finally {
        //  unitOfWork.end();
        // }
      }
    }

  }

  //  @Transactional
  public static void merge(EntityBase... entities) {
    for (EntityBase entity : entities)
      try {

        update(entity);
        //  em().flush();
        //entityManager.get().flush();

        //entityManager.get().getTransaction().commit();
        // log.debug("persisting entity " + entity.getClass().getSimpleName() + " " + entity);
        //   entityManager.get().getTransaction().commit();
      } catch (OptimisticLockException ole) {
        log.error("Unable to merge record" + ole);
      }

      catch (Exception | Error ex) {

        System.out.println("Unable to perform request in " + EM.class.getSimpleName() + ":persist, full stack trace follows:" + ex);
        ex.printStackTrace();

      }

  }

  //  @Transactional
  // @com.google.inject.persist.Transactional
  public static void beginUnitOfWork() {
    //try {
    unitOfWork.begin();
    //} catch (IllegalStateException ex) {
    //  unitOfWork.end();
    //unitOfWork.begin();

    // }
  }

  public static void detach(EntityBase... entities) {
    for (EntityBase entity : entities)
      try {

        //evict(entity);
        //entityManager.get().flush();

        //entityManager.get().getTransaction().commit();
        // log.debug("persisting entity " + entity.getClass().getSimpleName() + " " + entity);
        //   entityManager.get().getTransaction().commit();
      } catch (OptimisticLockException ole) {
        log.error("Unable to detach record" + ole);
      }

      catch (Exception | Error ex) {

        System.out.println("Unable to perform request in " + EM.class.getSimpleName() + ":persist, full stack trace follows:" + ex);
        ex.printStackTrace();

      }

  }

  //   @Transactional
  public static <T> void queryEach(Class<T> resultType, Visitor<T> handler, String queryStr, Object... params) {
    queryEach(resultType, handler, defaultBatchSize, queryStr, params);
  }

  // @Transactional
  public static <T> T find(Class<T> resultType, UUID id) {
    try {
      beginUnitOfWork();
      T result = em().find(resultType, id);
      return result;
    } finally {
      unitOfWork.end();
    }

  }

  // @Transactional
  public static <T> T namedQueryOne(Class<T> resultType, String namedQuery, Object... params) throws NoResultException {
    try {
      // unitOfWork.
      beginUnitOfWork();

      final TypedQuery<T> query = em().createNamedQuery(namedQuery, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      T result = query.getSingleResult();
      return result;
    } catch (Exception | Error ex) {

      // System.out.println("Unable to perform request in " + EM.class.getSimpleName() + ":persist, full stack trace follows:" + ex);
      // ex.printStackTrace();
      throw ex;
      // return null;

    } finally {
      unitOfWork.end();
    }

  }

  //  @Transactional
  public static <T> void queryEach(Class<T> resultType, Visitor<T> handler, int batchSize, String queryStr, Object... params) {
    try {
      beginUnitOfWork();

      final TypedQuery<T> query = em().createQuery(queryStr, resultType);
      if (params != null) {
        for (int i = 0; i < params.length; i++) {
          Object param = params[i];
          query.setParameter(i + 1, param); // JPA uses 1-based indexes
        }
      }
      query.setMaxResults(batchSize);
      for (int start = 0;; start += batchSize) {
        query.setFirstResult(start);
        final List<T> list = query.getResultList();
        if (list.isEmpty())
          return;
        for (T row : list) {
          if (!handler.handleItem(row))
            return;
        }
      }
    } finally {
      unitOfWork.end();
      // if (em() != null)
      //  em().close();

    }
  }
}
